Route computer and playwright calls directly to the VM - #157
Conversation
Add computer and playwright to the default browser routing allowlist so screenshot and execute traffic can skip the control plane once metro-api records activity on the kernel proxy.
The default now includes computer and playwright, not just curl.
The asserted list can grow; the test name should not.
rgarcia
left a comment
There was a problem hiding this comment.
QA’d commit 1b28d62 across sync and async clients.
- Browser-routing suite: 26 passed; PR CI is green.
- Focused experiments confirmed
computer/batch, clipboard, screenshot, and Playwright calls route directly with the session JWT and withoutAuthorization. process/*,fs/*, andtelemetry/eventsretain API-origin routing and API authorization.- Missing
base_url, custom allowlists, and the empty-value kill switch behave correctly. - Retry rewriting is idempotent and preserves request parameters.
Two non-blocking notes:
- The
computerprefix routes allcomputer/*endpoints, while the description and tests focus on screenshot/execute. Please consider one nested computer endpoint test. - A stale session JWT produces a direct-VM 401 with no automatic cache refresh or control-plane fallback. A follow-up could evict the route on 401/403 or document
client.browsers.get(id)as the refresh path.
No blocking findings.
A direct-to-VM 401/403 now evicts the cached route and retries the original request against the API.
|
addressed the stale JWT note: a direct-VM 401/403 now evicts the cached route and retries the original request on the control plane. no get() refresh — if the jwt is stale the API path still works, and a later create/retrieve will recache. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f525a24. Configure here.
Lint failed on the stale-JWT retry change.
A metro 401/403 with a jwt query param is enough to fall back to the control plane, so concurrent requests still retry after the first eviction.
|
addressed the concurrent stale-JWT race: retry now keys off the request jwt query param, not a still-present cache entry. first eviction no longer blocks other in-flight 401/403s from falling back to the control plane. same fallback is now in node #170 and go #164. |
rgarcia
left a comment
There was a problem hiding this comment.
The stale-JWT fallback still has the cache invalidation race now covered in the Node and Go reviews.
_session_id_from_direct_vm_response() identifies the current cached route by base URL, then cache.delete(session_id) removes it without checking whether its JWT is the one that failed. If request A leaves with jwt-old, another request refreshes the same base URL to jwt-new, and A later returns 401, the fresh jwt-new route is deleted. I reproduced this against head 728330d: after replacing the cached route before processing the old 401, cache.get("sess-1") returns None.
The latest change correctly fixes the separate race where concurrent stale 401s stopped retrying after the first eviction, but it does not protect a newer cache entry from an older response.
Please make eviction conditional on the current route’s base URL and JWT matching the failed request, and add a regression test that refreshes the route while the stale request is in flight and asserts the fresh route survives. The existing routing suite passes, but does not cover this interleaving.
A later 401 must not delete a route that was refreshed in flight.
|
fixed in 111aa09. eviction is now compare-and-delete on jwt: test: also ran this live against a real session before pushing — injected sorry for the earlier review ping — i was testing a workflow that requested reviews before bugbot / self-review landed. this commit is the actual fix. |
Sayan-
left a comment
There was a problem hiding this comment.
Verified at 111aa09. The race rgarcia flagged is fixed: delete_if_jwt refuses a mismatched JWT and still deletes a matching one, and with a refresh landing mid-flight the stale 401 leaves the fresh route alone (jwts sent to vm: ['jwt-STALE', 'jwt-FRESH'], fresh route intact). Suite green at 29, async path works.
Remaining issues are behavior drift from #173 (31517bb) and #167 (d5e0fc9). Same probes against all three heads:
Node Go Python
retries disabled falls back falls back 401, no fallback, stale route stays cached
budget spent by prior 500 falls back falls back 401, no fallback, stale route stays cached
backoff added to fallback none none ~390ms
fallback destination control plane control plane whatever the cache says now
-
The fallback is gated on the retry budget, with three consequences. It lives in
_should_retry, which the base client only consults whenremaining_retries > 0. Withmax_retries=0orwith_options(max_retries=0)the 401 surfaces raw. The two eviction sites are_should_retryand_process_response, and neither runs on a terminal 401, so the dead route stays cached and never self-heals (three successive screenshots all raisedAuthenticationErrorwith the stale route still present). And because the gate is the budget rather than the config, a transient 500 earlier in the same call reproduces both at default settings, so this is not limited to users who disable retries. Node and Go avoid all of this by falling back inside a single attempt. -
The fallback pays retry backoff, ~390ms measured from
INITIAL_RETRY_DELAYplus jitter, against the 176ms this change is meant to save. Node and Go retry immediately. -
The two SDK families promise different destinations. Under the race, Node and Go both go VM then control plane; here the retry re-runs
_prepare_optionsand re-routes to the VM on the refreshed JWT. Arguably better since it keeps the latency win, but worth making a decision rather than a side effect of hook placement.
Coverage: no nested computer/* test (nothing in the file matches clipboard, and computer/clipboard/read and write are the only three-segment tails that route direct), and the three new fallback tests are sync-only.

Summary
Default direct-to-VM routing now includes
computerandplaywrightin addition tocurlandtelemetry/stream.computer/screenshotandplaywright/executerewrite to{base_url}/...with the session JWT and dropAuthorization.process/*,fs/*, andtelemetry/eventsstay on the API origin.KERNEL_BROWSER_ROUTING_SUBRESOURCESstill overrides the default list. An empty value still disables routing.Metro-api activity recording for
/browser/kernel/*has been merged in kernel/kernel#3417. That was the hard gate for this allowlist flip. The kill switch is leavingbase_urlunset or settingKERNEL_BROWSER_ROUTING_SUBRESOURCESto empty.Latency (eu-west)
Same hop skip as the Node/Go SDKs. Comparison script: https://gist.github.com/tnsardesai/6704dfc86e2c0785e03beaf274aa672d
Same
eu-westsession. Published SDK still hitsapi.onkernel.com; this allowlist rewrite hits metro-api.Test plan
curl,telemetry/stream,computer,playwrightNote
Medium Risk
Changes default request routing and retry/auth-cache behavior for browser subresources, including 401/403 JWT eviction. Misrouting or over-retry could send traffic to the wrong origin or drop a valid cached route.
Overview
Default direct-to-VM routing now includes
computerandplaywright(in addition tocurlandtelemetry/stream). Screenshot and Playwright execute go to the session VM with JWT and noAuthorization;process,fs, andtelemetry/eventsstay on the control plane.KERNEL_BROWSER_ROUTING_SUBRESOURCESstill overrides or disables the list.On 401/403 from a JWT-query VM request, the client evicts that JWT from the route cache (not a newer one) and retries so the next hop can hit the API origin.
Reviewed by Cursor Bugbot for commit 111aa09. Bugbot is set up for automated code reviews on this repo. Configure here.